#!/bin/bash
#
# Copyright (C) 2016 Julien Desfossez <jdesfossez@efficios.com>
#
# SPDX-License-Identifier: GPL-2.0-only
#

TEST_DESC="Kernel tracer - select, poll and epoll payload extraction"

CURDIR=$(dirname "$0")/
TESTDIR=$CURDIR/../..
VALIDATE_SCRIPT="$CURDIR/validate_select_poll_epoll.py"

DISABLE_VALIDATE=0
# Babeltrace python bindings are required for the validation, but
# it is not a mandatory dependancy of the project, so fail run the
# without the content validation, at least we test that we are not
# crashing the kernel.
$VALIDATE_SCRIPT --help >/dev/null 2>&1
if test $? != 0; then
	echo "# Failed to run the validation script, Babeltrace Python bindings might be missing"
	DISABLE_VALIDATE=1
fi

LAST_WARNING=$(dmesg | grep " WARNING:" | cut -d' ' -f1 | tail -1)
LAST_OOPS=$(dmesg | grep " OOPS:" | cut -d' ' -f1 | tail -1)
LAST_BUG=$(dmesg | grep " BUG:" | cut -d' ' -f1 | tail -1)

SUPPORTED_SYSCALLS_LIST=$("$CURDIR"/select_poll_epoll --list-supported-test-syscalls)
SUPPORTED_SYSCALLS_COUNT=$(echo $SUPPORTED_SYSCALLS_LIST | awk -F '[\t,]' '{print NF}')

# Two tests validate their trace for every supported syscall
NUM_TESTS=$((88+(2*SUPPORTED_SYSCALLS_COUNT)))

# shellcheck source=../../utils/utils.sh
source $TESTDIR/utils/utils.sh

function check_trace_content()
{
	if test $DISABLE_VALIDATE == 1; then
		ok 0 "Validation skipped"
		return
	fi

	$VALIDATE_SCRIPT $@
	if test $? = 0; then
		ok 0 "Validation success"
	else
		fail "Validation"
	fi
}

function test_working_cases()
{
	SESSION_NAME="syscall_payload"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "Working cases for select, pselect6, poll, ppoll and epoll, waiting for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SUPPORTED_SYSCALLS_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t working_cases
	stop_lttng_tracing_ok

	validate_trace "$SUPPORTED_SYSCALLS_LIST" "$TRACE_PATH"
	check_trace_content -t working_cases --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH"

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_timeout_cases()
{
	SESSION_NAME="syscall_payload"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "Timeout cases (1ms) for select, pselect6, poll, ppoll and epoll"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME "$SUPPORTED_SYSCALLS_LIST"
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t working_cases_timeout
	stop_lttng_tracing_ok

	validate_trace "$SUPPORTED_SYSCALLS_LIST" "$TRACE_PATH"
	check_trace_content -t working_cases_timeout --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_pselect_invalid_fd()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="pselect6"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "pselect with invalid FD"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t pselect_invalid_fd
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t pselect_invalid_fd --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_ppoll_big()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="ppoll"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "ppoll with 2047 FDs"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t ppoll_big
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t ppoll_big --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_ppoll_fds_buffer_overflow()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="ppoll"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "ppoll buffer overflow, should segfault, waits for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	diag "Expect segfaults"
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t ppoll_fds_buffer_overflow
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"

	check_trace_content -t ppoll_fds_buffer_overflow --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_pselect_invalid_pointer()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="pselect6"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "pselect with invalid pointer, waits for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t pselect_invalid_pointer
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t pselect_invalid_pointer --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_ppoll_fds_ulong_max()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="ppoll"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "ppoll with ulong_max fds, waits for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t ppoll_fds_ulong_max
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t ppoll_fds_ulong_max --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_epoll_pwait_invalid_pointer()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="epoll_pwait"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "epoll_pwait with invalid pointer, waits for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t epoll_pwait_invalid_pointer
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t epoll_pwait_invalid_pointer --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_epoll_pwait_fds_int_max()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="epoll_pwait"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "epoll_pwait with maxevents set to INT_MAX, waits for input"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t epoll_pwait_int_max
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t epoll_pwait_int_max --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_ppoll_concurrent_write()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="ppoll"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "ppoll with concurrent updates of the structure from user-space, stress test (3000 iterations), waits for input + timeout 1ms"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t ppoll_concurrent_write
	stop_lttng_tracing_ok

	validate_trace "$SYSCALL_LIST" "$TRACE_PATH"
	check_trace_content -t ppoll_concurrent_write --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

function test_epoll_pwait_concurrent_unmap()
{
	SESSION_NAME="syscall_payload"
	local SYSCALL_LIST="epoll_ctl,epoll_pwait"
	TRACE_PATH=$(mktemp -d -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_trace_path.XXXXXX")
	TEST_VALIDATION_OUTPUT_PATH=$(mktemp -u -t "tmp.test_kernel_select_poll_epoll_${FUNCNAME[0]}_validation.XXXXXX")

	diag "epoll_pwait with concurrent munmap of the buffer from user-space, should randomly segfault, run multiple times, waits for input + timeout 1ms"

	create_lttng_session_ok $SESSION_NAME "$TRACE_PATH"

	lttng_enable_kernel_syscall_ok $SESSION_NAME $SYSCALL_LIST
	add_context_kernel_ok $SESSION_NAME channel0 pid

	start_lttng_tracing_ok
	diag "Expect segfaults"
	for i in $(seq 1 100); do
		yes | "$CURDIR"/select_poll_epoll --validation-file "$TEST_VALIDATION_OUTPUT_PATH" -t epoll_pwait_concurrent_munmap
	done
	stop_lttng_tracing_ok

	# epoll_wait is not always generated in the trace (stress test)
	validate_trace "epoll_ctl" "$TRACE_PATH"
	check_trace_content -t epoll_pwait_concurrent_munmap --validation-file "$TEST_VALIDATION_OUTPUT_PATH" "$TRACE_PATH" 2>/dev/null

	destroy_lttng_session_ok $SESSION_NAME

	rm -rf "$TRACE_PATH"
	rm -f "$TEST_VALIDATION_OUTPUT_PATH"
}

# MUST set TESTDIR before calling those functions
plan_tests $NUM_TESTS

print_test_banner "$TEST_DESC"

# Only run this test on x86 and arm
uname -m | grep -E "x86|i686|arm|aarch64" >/dev/null 2>&1
if test $? != 0; then
	skip 0 "Run only on x86 and arm. Skipping all tests." $NUM_TESTS
	exit 0
fi

diag "Supported syscalls are $SUPPORTED_SYSCALLS_LIST"

check_skip_kernel_test "$NUM_TESTS" "Skipping all tests." ||
{
	validate_lttng_modules_present

	start_lttng_sessiond

	test_working_cases
	test_timeout_cases
	test_pselect_invalid_fd
	test_ppoll_big
	test_ppoll_fds_buffer_overflow
	test_pselect_invalid_pointer
	test_ppoll_fds_ulong_max
	test_epoll_pwait_invalid_pointer
	test_epoll_pwait_fds_int_max
	test_ppoll_concurrent_write
	test_epoll_pwait_concurrent_unmap

	stop_lttng_sessiond

	NEW_WARNING=$(dmesg | grep " WARNING:" | cut -d' ' -f1 | tail -1)
	NEW_OOPS=$(dmesg | grep " OOPS:" | cut -d' ' -f1 | tail -1)
	NEW_BUG=$(dmesg | grep " BUG:" | cut -d' ' -f1 | tail -1)

	if test "$LAST_WARNING" != "$NEW_WARNING"; then
		diag "Last WARNING before tests: ${LAST_WARNING}"
		diag "Last WARNING after tests: ${NEW_WARNING}"
		fail "New WARNING generated"
	fi
	if test "$LAST_OOPS" != "$NEW_OOPS"; then
		diag "Last OOPS before tests: ${LAST_OOPS}"
		diag "Last OOPS after tests: ${NEW_OOPS}"
		fail "New OOPS generated"
	fi
	if test "$LAST_BUG" != "$NEW_BUG"; then
		diag "Last BUG before tests: ${LAST_BUG}"
		diag "Last BUG after tests: ${NEW_BUG}"
		fail "New BUG generated"
	fi
}
